/*-
 * Copyright (c) 2006 Vishal Patil <bsd.devil@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <assert.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include "iscsi_tools.h"
#include "iscsi_pdu.h"
#include "iscsi_util.h"
#include "iscsi_pdu_list.h"
#include "iscsi_login_params.h"

/* Buffer for sending and reciving data */
uint8_t buffer[1024];

/*
 *	Currently the authentication type is None
 */
static struct iscsi_pdu_incore *
create_iscsi_login_pdu(struct iscsi_session * sess) {

	struct iscsi_login_hdr * login_hdr;
	struct iscsi_pdu_incore * pdu;
	struct iscsi_login_params * params;
	char value[16];
	
	params = sess->login_params;

	pdu = get_pdu(0,256);
	login_hdr = &pdu->pdu_hdr.login_hdr;
	login_hdr->opcode = ISCSI_LOGIN_CMD  | ISCSI_OP_IMMEDIATE;
	memcpy(login_hdr->isid,sess->isid,sizeof(sess->isid));
	login_hdr->cmdsn = htonl(sess->cmdsn);
	login_hdr->expstatsn = htonl(sess->active_conn->expstatsn);
	login_hdr->tsih = 0x0;
	login_hdr->itt = ISCSI_LOGIN_ITT;
	login_hdr->cid = htons(sess->active_conn->cid); 

	if (sess->active_conn->login_state == ISCSI_SECNEGO_STATE) {
		login_hdr->flags  = 	ISCSI_FLAG_LOGIN_TRANSIT | 
					(ISCSI_FLAG_LOGIN_SECNEGO << 2) |
					ISCSI_FLAG_LOGIN_OPNEGO ; 
		add_key_value(pdu,"InitiatorName",params->initiatorName);
		add_key_value(pdu,"SessionType",params->sessionType);
		add_key_value(pdu,"AuthMethod",params->authMethod);	
	} else 
	if (sess->active_conn->login_state == ISCSI_OPNEGO_STATE) {
		login_hdr->flags  = 	ISCSI_FLAG_LOGIN_TRANSIT | 
					(ISCSI_FLAG_LOGIN_OPNEGO << 2) |
					ISCSI_FLAG_LOGIN_FULL; 

		add_key_value(pdu,"HeaderDigest",params->headerDigest);
		add_key_value(pdu,"DataDigest",params->dataDigest);

		itoa(value,sess->login_params->maxRecvDataSegmentLength);
		add_key_value(pdu,"MaxRecvDataSegmentLength",value);
		
		itoa(value,sess->login_params->defaultTime2Wait);
		add_key_value(pdu,"DefaultTime2Wait",value);

		itoa(value,sess->login_params->defaultTime2Retain);
		add_key_value(pdu,"DefaultTime2Retain",value);
	}
	
	
	return pdu;
}

/*
 *	Handle login response PDU
 */
int
handle_login_response(struct iscsi_session * sess, struct 
		iscsi_pdu_incore * pdu) 
{
	char value[256];
	struct iscsi_login_rsp_hdr * login_rsp_hdr;

	login_rsp_hdr = &pdu->pdu_hdr.login_rsp_hdr;

	/* Check the login status */
	if (login_rsp_hdr->status_class != ISCSI_STATUS_CLS_SUCCESS) {
		fprintf(stderr,"Login not successfull\n");
		return -1;
	}

	/* Check the status SN*/
	if (ntohl(login_rsp_hdr->statsn) != sess->active_conn->expstatsn) {
		fprintf(stderr,"Unexpected status sn 0x%x\n",
				login_rsp_hdr->statsn);
		return -1;
	} else {
		sess->active_conn->expstatsn++;
	}
	
	/* Check for opcode */
	if (login_rsp_hdr->opcode != ISCSI_LOGIN_RSP) {
		fprintf(stderr,"Invalid login response PDU\n");
		return -1;
	}

	/* Check for ITT */
	if (login_rsp_hdr->itt != ISCSI_LOGIN_ITT) {
		fprintf(stderr,"Invalid ITT in response 0x%x\n",
				login_rsp_hdr->itt);
		return -1; 
	}
	
	if (sess->active_conn->login_state == ISCSI_SECNEGO_STATE) {
		/* Check CSG */
		if (ISCSI_LOGIN_CURRENT_STAGE(login_rsp_hdr->flags) !=
			ISCSI_FLAG_LOGIN_SECNEGO) {
			fprintf(stderr,"Invalid CSG in login response\n");
			return -1;
		}		
		/* Target Portal Group Tag must be present in the first
		 * Login response
		 */
		if (get_key_value("TargetPortalGroupTag",
					pdu,value) != 1) {
			fprintf(stderr,"TargetPortalGroupTag not"
			" in the first login response\n");
			return -1;
		}
		sess->login_params->targetPortalGroupTag  = atoi(value); 
	
		/* Check the authentication parameter value */
		if (get_key_value("AuthMethod",
					pdu,value) != 1) {
			fprintf(stderr,"AuthMethod not"
			" in the first login response\n");
			return -1;
		}	
		
	       	/* Check if the target agrees with the auth method*/	
		if (strcmp(sess->login_params->authMethod,value) != 0) {
			fprintf(stderr,"AuthMethod does not"
			" match with that of the target\n");
			return -1;
		}
		if (ISCSI_LOGIN_NEXT_STAGE(login_rsp_hdr->flags)
				    == ISCSI_FLAG_LOGIN_OPNEGO) {
			sess->active_conn->login_state = 
				ISCSI_OPNEGO_STATE;
		}
	} else 
	if (sess->active_conn->login_state == ISCSI_OPNEGO_STATE) {
		/* Check CSG*/
		if (ISCSI_LOGIN_CURRENT_STAGE(login_rsp_hdr->flags) !=
			ISCSI_FLAG_LOGIN_OPNEGO) {
			fprintf(stderr,"Invalid CSG in login response\n");
		}

		if (ISCSI_LOGIN_NEXT_STAGE(login_rsp_hdr->flags)
				    == ISCSI_FLAG_LOGIN_FULL) {
			sess->active_conn->login_state =
				ISCSI_FULL_FEATURED_STATE;
			sess->active_conn->conn_state = 
				ISCSI_CONN_LOGGED_IN;
			sess->state = ISCSI_SESS_LOGGED_IN;
		}
	}

	sess->expcmdsn = ntohl(login_rsp_hdr->expcmdsn);
	sess->maxcmdsn = ntohl(login_rsp_hdr->maxcmdsn);
	set_session_params(sess,pdu);
	return 0;
}

/*
 *	Send PDU to iSCSI Network Entity
 */
int
send_pdu(int sock, struct iscsi_pdu_incore * pdu) {
	int pdu_size = pdu_to_buffer(pdu,buffer,sizeof(buffer));
			
	if (pdu_size == -1) {
		return -1;
	}
	return send(sock,buffer,pdu_size,0);	
}

/*
 *	Receive PDU from a iSCSI Network Entity
 */
struct iscsi_pdu_incore *  
recv_pdu(int sock) {
	
	bzero(buffer,sizeof(buffer));

	if (recv(sock,buffer,sizeof(buffer),0) == -1) {
		return NULL;
	}
	
	return buffer_to_pdu(buffer,sizeof(buffer));
}

/*
 *	Logout of the network entity and close the session
 */
int 
logout(struct iscsi_session * sess) {
	int sock = sess->active_conn->sock;
	struct iscsi_logout_hdr * logout_hdr;
	struct iscsi_logout_rsp_hdr * logout_rsp_hdr;
	struct iscsi_pdu_incore * pdu;

	/* Make sure we are logged in */
	assert(sess->active_conn->conn_state == ISCSI_CONN_LOGGED_IN);
	
	while (sess->active_conn->conn_state != ISCSI_CONN_CLEANUP_WAIT) {
		sess->active_conn->conn_state = ISCSI_CONN_LOGGING_OUT;

		/* Construct the logout PDU here */
		pdu = get_pdu(0,0);
		logout_hdr = &pdu->pdu_hdr.logout_hdr;
		logout_hdr->opcode = ISCSI_LOGOUT_CMD | ISCSI_OP_IMMEDIATE;
		logout_hdr->reason = ISCSI_LOGOUT_REASON_CLOSE_SESSION;
		logout_hdr->cmdsn = htonl(sess->cmdsn);
		logout_hdr->expstatsn = 
			htonl(sess->active_conn->expstatsn);
		logout_hdr->itt = ISCSI_LOGOUT_ITT;
		logout_hdr->cid = htons(sess->active_conn->cid); 

		if (send_pdu(sess->active_conn->sock,pdu) == -1) {
			perror("Problem sending the iSCSI"
				" logout PDU");
			return -1;
		}
		sess->active_conn->conn_state = 
				ISCSI_CONN_LOGOUT_REQUESTED;
			
		/* Receive the pdu */	
		pdu = recv_pdu(sock);
		if (!pdu) {
			fprintf(stderr,"Problem receiving the PDU "
				" while logging out\n");
			put_pdu(pdu);
			return -1;
		}

		logout_rsp_hdr = &pdu->pdu_hdr.logout_rsp_hdr; 
		if (logout_rsp_hdr->opcode != ISCSI_LOGOUT_TARGET_OPCODE ||
			logout_rsp_hdr->response != ISCSI_LOGOUT_SUCCESS) {
			fprintf(stderr,"Target had problems "
				" while logging out, code:0x%x\n",
				logout_rsp_hdr->opcode);
			put_pdu(pdu);
			return -1;
		}
		put_pdu(pdu);
		sess->active_conn->conn_state = ISCSI_CONN_CLEANUP_WAIT;
		sess->state = ISCSI_SESS_FREE;
	}
	return 0;	
}

/*
 *	Log into the network entity
 */
int
login(struct iscsi_session * sess) {
	
	int sock = sess->active_conn->sock;
	struct iscsi_pdu_incore * pdu;

	/* Make sure we are in a correct state */
	assert(sess->active_conn->conn_state == ISCSI_CONN_LOGGING_IN);
	
	while (sess->active_conn->conn_state != ISCSI_CONN_LOGGED_IN
		&&  sess->active_conn->conn_state != ISCSI_CONN_FREE) {	
		
		pdu = create_iscsi_login_pdu(sess);
		if (send_pdu(sock,pdu) == -1) {
			fprintf(stderr,"Problem sending the PDU\n");
			sess->active_conn->conn_state = ISCSI_CONN_FREE;
		}
		
		/* Receive the pdu */	
		pdu = recv_pdu(sock);
		if (!pdu) {
			fprintf(stderr,"Problem receiving the PDU "
				" while logging in\n");
			sess->active_conn->conn_state = ISCSI_CONN_FREE;
			put_pdu(pdu);
			return -1;
		}
		
		if (handle_login_response(sess,pdu) == -1) {
			fprintf(stderr,"Invalid login response from the "
				" network entity\n");
			sess->active_conn->conn_state = ISCSI_CONN_FREE;
			put_pdu(pdu);
			return -1;
		}
		put_pdu(pdu);
	}
	
	return 0;
}

/*
 *	Create and send the iSCSI PDU containing the SendTargets=ALL
 *	text command
 */
int 
sendtargets(struct iscsi_session * sess) {
	struct iscsi_pdu_incore * pdu;	
	struct iscsi_txt_hdr * txt_hdr;
	struct iscsi_txt_rsp_hdr * txt_rsp_hdr;
	
	assert(sess->active_conn->conn_state == ISCSI_CONN_LOGGED_IN);
	int sock = sess->active_conn->sock;

	/* Create the SendTargets=All PDU */	
	pdu = get_pdu(0,48);
	txt_hdr = &pdu->pdu_hdr.txt_hdr;
	txt_hdr->opcode = ISCSI_TEXT_CMD | ISCSI_OP_IMMEDIATE;	
	txt_hdr->flags |= ISCSI_FLAG_TEXT_FINAL;
	txt_hdr->itt = sess->next_itt++;
	txt_hdr->ttt = ISCSI_DEFAULT_TTT;
	txt_hdr->cmdsn = htonl(sess->cmdsn);
	txt_hdr->expstatsn = htonl(sess->active_conn->expstatsn);
	add_key_value(pdu,"SendTargets","All");
	
	if (send_pdu(sock,pdu) == -1) {
		fprintf(stderr,"Problem sending the sendTargets PDU\n");
		sess->active_conn->conn_state = ISCSI_CONN_FREE;
		return -1;
	}
	int done = 0;

	while(!done) {	
		/* Receive the pdu */	
		pdu = recv_pdu(sock);
		if (!pdu) {
			fprintf(stderr,"Problem receiving the sendTargets"
				       	"PDU while logging in\n");
			sess->active_conn->conn_state = ISCSI_CONN_FREE;
			put_pdu(pdu);
			return -1;
		}

		txt_rsp_hdr = &pdu->pdu_hdr.txt_rsp_hdr;	
		
		/* Check the text response opcode */
		if (txt_rsp_hdr->opcode != ISCSI_TEXT_RSP) {
			fprintf(stderr,"Did not get a text response after "
				" sending the SendTargets pdu\n");
			sess->active_conn->conn_state = ISCSI_CONN_FREE;
			return -1;
		}

		/* Check the status SN */
		if (ntohl(txt_rsp_hdr->statsn) != 
				sess->active_conn->expstatsn) {
			fprintf(stderr,"Unexpected status sn 0x%x\n",                                		txt_rsp_hdr->statsn);
			return -1;
		} else {
			sess->active_conn->expstatsn++;
		}
		
		if (txt_rsp_hdr->ttt == ISCSI_DEFAULT_TTT) {
			done = 1;
		}

		/* Print SendTargets output */
		char * buf = (char *)pdu->data_seg;
		int len;
		
		len = strlen(buf);
		while (len) {
			len = strlen(buf);
			printf("%s\n",buf);
			buf = buf + len + 1;		
		}
		put_pdu(pdu);
	}
	return 0;
}

/*
 *	Create a TCP/IP connection to the Network Entity. Currently
 *	this creates a IPv4 based connection
 */
int
create_connection(struct iscsi_session * sess) {
	int sock = 0;
	struct sockaddr_in serv;
	struct hostent * hp;
	struct iscsi_conn * conn;
	
	conn = (struct iscsi_conn *)malloc(sizeof(struct iscsi_conn));
	memset(conn,0,sizeof(*conn));
	
	assert(conn->conn_state == ISCSI_CONN_FREE);

	char * host 	= sess->login_params->targetAddress;
	int port	= sess->login_params->port;
	
	/* Init stuff */
	bzero((void *)&serv,sizeof(struct sockaddr_in));
	serv.sin_family = AF_INET;	
	serv.sin_port = htons(port);	

	hp = gethostbyname(host);
	if (hp == NULL) {
		herror("Could not find the host information");
		return -1;
	}
	
	memcpy((void *)&serv.sin_addr,hp->h_addr,hp->h_length);

	sock = socket(PF_INET, SOCK_STREAM, 0); 
	if (sock < 0) {
		perror("Problem creating socket");
		return -1;
	}

	/* Wait before making the connection */
	conn->conn_state = ISCSI_CONN_XPT_WAIT;

	if (connect(sock,(struct sockaddr *)&serv,sizeof(serv))) {
		perror("Problem connecting to remote entity");
		return -1;
	}		
	conn->sock = sock;

	/* Connection(s) successfully established */	
	conn->conn_state = ISCSI_CONN_LOGGING_IN;

	iscsi_session_add_conn(sess,conn);
	return 0;
}
/*
 *	Tear down the TCP/IP connection to the Network Entity
 */
int 
close_connection(struct iscsi_session * sess) {
	int sock = sess->active_conn->sock;

	/* Make sure we are in the right state */
	assert(sess->active_conn->conn_state == 
			ISCSI_CONN_CLEANUP_WAIT);
	
	if (sock) {
		close(sock);
	}
	sess->active_conn->conn_state = ISCSI_CONN_CLEANUP_WAIT;

	return 0;
}


